﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.Runtime.CompilerServices;
using System.Threading;

namespace O1.Kernel.Storage.Transactions
{
    [ContractVerification(true)]
    public static class MultiVersion
    {
        public const long Infinity = long.MaxValue;

        private const int DmlStatusShift = 63;

        private static readonly HashSet<long> RetiredEarly = new HashSet<long>();

        private static readonly object Sync = new object();

        private static long counter;

        private static long watermark;

        public static long Watermark
        {
            get
            {
                return watermark;
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static MultiVersionState State(this long versionId)
        {
            Contract.Assume((versionId >> DmlStatusShift) < 2);
            Contract.Assume((versionId >> DmlStatusShift) > -1);
            
            // A version is pending and not visible     : Indicates in-flight, cell is 'locked' for writing, change is not visible
            // B version is committed and not visible   : Indicates in-flight, cell is 'locked' for writing, change is not visible
            // C version is pending and is visible      : Indicates aborted, eligible for GC, change is not visible
            // D version is committed and visible       : Indicates a committed version, change is visible
            var visible = versionId.IsVisible();
            if (versionId.IsPending())
            {
                if (visible)
                {
                    return MultiVersionState.Aborted;
                }

                return MultiVersionState.Pending;
            }

            if (!visible)
            {
                return MultiVersionState.Pending;
            }

            return MultiVersionState.Committed;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool IsCommitted(this long transactionId)
        {
            return transactionId < 0;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool IsPending(this long transactionId)
        {
            return transactionId > -1;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static long AsCommitted(this long transactionId)
        {
            return transactionId | ~Infinity;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static long AsPending(this long transactionId)
        {
            return transactionId & Infinity;
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static long PendingOwner<T>(this MultiVersion<T>[] versions, long versionId)
        {
            if (versions == null)
            {
                return versionId;
            }

            if (versions.Length == 0)
            {
                return versionId;
            }

            var comparand = versions[0].VersionId;
            if (comparand.State() != MultiVersionState.Pending)
            {
                return versionId;
            }

            return comparand;
        }

        public static void SetRecoveryCheckpoint(long recoveryCheckpoint)
        {
            Contract.Requires(recoveryCheckpoint > 0);

            lock (Sync)
            {
                counter = recoveryCheckpoint.AsPending();
                watermark = recoveryCheckpoint.AsCommitted();
            }
        }

        public static long Next()
        {
            Contract.Ensures(Contract.Result<long>() > 0);

            return Interlocked.Increment(ref counter);
        }

        public static void Retire(this long versionId)
        {
            // A long running Tx can block the counter for some time, and transactions can be retired out of order.
            // An expectation must be set for outstanding transactions that are not retired.
            // Spec:
            //      1. If the target TX to retire is sequential to the last to be retired, all is well. Happy path.
            //      2. If the target TX is less than the last to be retired, there is a serious Tx sync problem. Throw?
            //      3. If the target TX is greater than the next to retire (retired +1)... it needs to be known as committed.
            versionId = versionId.AsCommitted();
            lock (Sync)
            {
                versionId = versionId.AsPending();
                if (watermark + 1 == versionId)
                {
                    watermark = versionId;
                    return;
                }

                RetiredEarly.Add(versionId);

                var next = watermark + 1;
                for (;;)
                {
                    if (!RetiredEarly.Contains(next))
                    {
                        break;
                    }

                    RetiredEarly.Remove(next);
                    watermark = next;
                    next = watermark + 1;
                }
            }
        }

        [MethodImpl(MethodImplOptions.AggressiveInlining)]
        public static bool IsVisible(this long versionId)
        {
            lock (Sync)
            {
                var max = watermark;
                if (versionId <= max)
                {
                    return true;
                }

                if (RetiredEarly.Contains(versionId))
                {
                    return true;
                }
            }

            return false;
        }

        [ContractInvariantMethod]
        private static void ObjectInvariant()
        {
            Contract.Invariant(counter > 0);
        }
    }
}